package name.koppernickus.lsp.analyzer;

import com.google.common.base.Predicate;
import com.google.common.collect.Sets;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Sets.filter;
import static com.google.common.collect.Sets.newHashSet;
import static name.koppernickus.lsp.analyzer.OverridableMethodSignature.method;

public class Subclassing {

    private final Class baseClass;
    private final Class subClass;

    public Subclassing(Class aClass) {
        checkNotNull(aClass);
        Class superclass = aClass.getSuperclass();
        checkNotNull(superclass);
        this.baseClass = superclass;
        this.subClass = aClass;
    }

    public Class getBaseClass() {
        return baseClass;
    }

    public Class getSubClass() {
        return subClass;
    }

    public boolean isProperSubclassing() {
        return baseClass != Object.class;
    }

    public boolean isAbstractSubclassing() {
        return isAbstract(baseClass);
    }

    public boolean isConcreteSubclassing() {
        return isProperSubclassing() &&
               !isAbstractSubclassing();
    }

    public boolean isExceptionsSubclassing() {
        return Throwable.class.isAssignableFrom(baseClass) && Throwable.class.isAssignableFrom(subClass);
    }

    public boolean isOverridingSubclassing() {
        return overridenMethods().size() > 0;
    }

    public boolean isProperOverridingSubclassing() {
        return removeEqualsHashcodeAndToString(overridenMethods()).size() > 0;
    }

    private Set<OverridableMethodSignature> removeEqualsHashcodeAndToString(Set<OverridableMethodSignature> methods) {
        Set<OverridableMethodSignature> equalsHashCodeAndToString = newHashSet(
                method("equals", Object.class), method("hashCode"), method("toString")
        );
        return Sets.difference(methods, equalsHashCodeAndToString);
    }

    public Set<OverridableMethodSignature> overridenMethods() {
        Set<OverridableMethodSignature> result = new HashSet<OverridableMethodSignature>();
        for (Method method : subClass.getDeclaredMethods()) {
            if (isOverriden(method)) {
                result.add(new OverridableMethodSignature(method));
            }
        }
        return result;
    }

    private boolean isOverriden(Method method) {
        if (!isPublicOrProtected(method)) {
            return false;
        }
        return isDefinedInClassOrAnySuperclass(method.getDeclaringClass().getSuperclass(), new OverridableMethodSignature(method));
    }

    private boolean isDefinedInClassOrAnySuperclass(Class subClass, OverridableMethodSignature method) {
        if (subClass == null) {
            return false;
        }
        for (Method declaredMethod : subClass.getDeclaredMethods()) {
            if (method.equals(new OverridableMethodSignature(declaredMethod))) {
                return !isAbstract(declaredMethod);
            }
        }
        return isDefinedInClassOrAnySuperclass(subClass.getSuperclass(), method);
    }

    private boolean isAbstract(Class aClass) {
        return Modifier.isAbstract(aClass.getModifiers());
    }

    private boolean isPublicOrProtected(Method aMethod) {
        return Modifier.isPublic(aMethod.getModifiers()) || Modifier.isProtected(aMethod.getModifiers());
    }

    private boolean isAbstract(Method aMethod) {
        return Modifier.isAbstract(aMethod.getModifiers());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Subclassing that = (Subclassing) o;
        if (!baseClass.equals(that.baseClass)) return false;
        if (!subClass.equals(that.subClass)) return false;
        return true;
    }

    @Override
    public int hashCode() {
        int result = baseClass.hashCode();
        result = 31 * result + subClass.hashCode();
        return result;
    }

    @Override
    public String toString() {
        return "class " + subClass.getName() + " extends " + baseClass.getName();
    }

}
